//******************************************************************************
//this code is protected by the GNU affero GPLv3
//author:Sylvain BERTRAND <sylvain.bertrand AT gmail dot com>
//                        <digital.ragnarok AT gmail dot com>
//******************************************************************************
#include "globals.h"

#define DCE_ERR(fmt,...) \
{\
void *sysname=(void*)udev_device_get_sysname(o->d);\
PERR("output:%s:dce:" fmt,sysname,##__VA_ARGS__);\
}

#define DCE_LOG(fmt,...) \
{\
void *sysname=(void*)udev_device_get_sysname(o->d);\
LOG("output:%s:dce:" fmt,sysname,##__VA_ARGS__);\
}

u8 v_refresh(struct alga_timing *timing)
{
  f32 line_pixels=timing->h+timing->h_bl;
  f32 frame_lines=timing->v+timing->v_bl;
  f32 frame_pixels=frame_lines*line_pixels;
  f32 r=(f32)(timing->pixel_clk)*1000/frame_pixels;
  return (u8)f32_round_near(r);
}

//XXX:here, should pick a pixel format for the frame buffer in regards
//of the pixel depth the display device can handle, more than 8 bits
//per color component may be welcome, in sRGB color space.
//*BUT*, toolkits have constraints on their pixel format support, may have to
//perform pixel conversion on the fly.
static u8 dce_pixel_fmt_select_heuristics(u8 blk_idx,
                                                   struct si_dce_dps_info *info)
{
  //prefer 10 bpc, ARGB, sRGB
  u8 j=0;
  while(1){
    if(j==ALGA_PIXEL_FMTS_MAX) break;
    if(info->pixel_fmts[blk_idx][j]==ALGA_PIXEL_FMT_INVALID) break;
    if(info->pixel_fmts[blk_idx][j]==ALGA_ARGB2101010) return ALGA_ARGB2101010;
    ++j;
  }
  //then the very common 8 bpc, ARGB, sRGB
  j=0;
  while(1){
    if(j==ALGA_PIXEL_FMTS_MAX) break;
    if(info->pixel_fmts[blk_idx][j]==ALGA_PIXEL_FMT_INVALID) break;
    if(info->pixel_fmts[blk_idx][j]==ALGA_ARGB8888) return ALGA_ARGB8888;
    ++j;
  }
  //erf... minimal support then... 6 bpc? ARGB, sRGB
  j=0;
  while(1){
    if(j==ALGA_PIXEL_FMTS_MAX) break;
    if(info->pixel_fmts[blk_idx][j]==ALGA_PIXEL_FMT_INVALID) break;
    if(info->pixel_fmts[blk_idx][j]==ALGA_ARGB6666) return ALGA_ARGB6666;
    ++j;
  }
  return ALGA_PIXEL_FMT_INVALID;//ouch!
}

s8 dce_info_get(s32 output_name)
{
  struct output *o=&registry[output_name].output;

  ul req=IOR('d',SI_DCE_DPS_INFO,o->dce_info);
  l r=ioctl(o->fd,req,&o->dce_info);
  if(ISERR(r)){
    DCE_ERR("unable to get info\n");
    return LWL_ERR;
  }

  DCE_LOG("got information from the driving hardware\n");
  return LWL_OK;
}

//in case something is wrong
static void alga_timing_to_str(void *mode_str,struct alga_timing *timing)
{
  memset(mode_str,0,sizeof("hhhhxvvvv@rrr"));
  snprintf(mode_str,sizeof("hhhhxvvvv@rrr"),"%ux%u@%u",timing->h,timing->v,
                                                             v_refresh(timing));
}

s8 dce_mode_select(s32 output_name,void *cfg_mode)
{
  s8 r=LWL_OK;
  struct output *o=&registry[output_name].output;
  struct alga_timing *o_timings=&o->dce_info.timings[o->blk_idx][0];
  void *sysname=(void*)udev_device_get_sysname(o->d);

  u8 timing_idx=0;
  if(*(u8*)cfg_mode!=0){//if cfg file did provide a mode
    while(1){
      if(timing_idx==ALGA_TIMINGS_MAX) break;
      if(o_timings[timing_idx].pixel_clk==0) break;
      u8 mode_str[sizeof("hhhhxvvvv@rrr")];
      alga_timing_to_str(&mode_str[0],&o_timings[timing_idx]);
      DCE_LOG("have mode:%s\n",&mode_str[0]);
      if(strncmp(mode_str,cfg_mode,sizeof("hhhhxvvvv@rrr")-1)==0){
        DCE_LOG("mode %s match provided config mode\n",&mode_str[0]);
        break;
      }
      ++timing_idx;
    }
    //no timing matching the one provided by cfg file, choose the default one
    if(timing_idx==ALGA_TIMINGS_MAX||o_timings[timing_idx].pixel_clk==0){
      DCE_LOG("config mode %s was not match, selecting mode\n", cfg_mode);
      timing_idx=0;
    }
  }

  o->current=&o_timings[timing_idx];

  o->pixel_fmt=dce_pixel_fmt_select_heuristics(o->blk_idx,&o->dce_info);

  if(o->pixel_fmt==ALGA_PIXEL_FMT_INVALID){
    PERR("output:%s:dce:no pixel format supported\n",sysname);
    r=LWL_ERR;goto exit;
  }

  LOG("output:%s:dce:video mode selected is %ux%u@%u with %s pixel"
       " format\n",sysname,o->current->h,o->current->v,v_refresh(o->current),
                                             alga_pixel_fmts_str[o->pixel_fmt]);
exit:
  return r;
}

s8 dce_mode_program(s32 output_name)
{
  struct output *o=&registry[output_name].output;
  void *sysname=(void*)udev_device_get_sysname(o->d);

  //----------------------------------------------------------------------------
  //alloc a properly sized double buffered frame buffer
  o->fb.align=PAGE_SZ;//align on a cpu memory page
  u64 sz=2*(o->current->h*o->current->v*alga_pixel_fmts_sz[o->pixel_fmt]);
  o->fb.sz=sz;
  ul req=IOWR('d',SI_MEM_ALLOC,o->fb);
  l r=ioctl(o->fd,req,(l)&o->fb);
  if(ISERR(r)){
    PERR("output:%s:dce:unable to allocate proper vram for a frame buffer of"
                                         " size %llu bytes\n",sysname,o->fb.sz);
    goto err;
  }

  LOG("output:%s:dce:double buffered frame buffer of %llu bytes allocated\n",
                                                                    sysname,sz);
  //----------------------------------------------------------------------------

  //----------------------------------------------------------------------------
  //program synchronously the video mode
  struct si_dce_dp_set dp_set;
  dp_set.idx=o->blk_idx;
  dp_set.primary=o->fb.gpu_addr;
  dp_set.secondary=o->fb.gpu_addr+sz/2;
  dp_set.pixel_fmt=o->pixel_fmt;
  dp_set.timing=*(o->current);
  dp_set.pitch=o->current->h;
  req=IOW('d',SI_DCE_DP_SET,dp_set);
  r=ioctl(o->fd,req,(l)&dp_set);
  if(ISERR(r)){
    PERR("output:%s:dce:%ux%u@%u: unable to program\n",sysname,o->current->h,
                                         o->current->v,v_refresh(o->current));
    goto err_free_fb;
  }
  //----------------------------------------------------------------------------

  LOG("output:%s:dce:%ux%u@%u:programmed successfully\n",sysname,o->current->h,
                                         o->current->v,v_refresh(o->current));
  return LWL_OK;

err_free_fb:
  req=IOW('d',SI_MEM_FREE,o->fb.gpu_addr);
  r=ioctl(o->fd,req,&o->fb.gpu_addr);
  if(ISERR(r))
    PERR("output:%s:dce:unable to free frame buffer memory (LEAK!)\n",sysname);
err:
  return LWL_ERR; 
}

void dce_dpm_on(struct output *o)
{
  void *sysname=(void*)udev_device_get_sysname(o->d);

  ul req=IOR('d',SI_DCE_DP_DPM,o->blk_idx);
  l r=ioctl(o->fd,req,&o->blk_idx);
  if(ISERR(r)) PERR("output:%s:dce:unable to swich dpm to on\n",sysname);
  else LOG("output:%s:dce:dpm on successfully\n",sysname);
}

void dce_double_frame_buffer_free(struct output *o)
{
  void *sysname=(void*)udev_device_get_sysname(o->d);

  ul req=IOR('d',SI_MEM_FREE,o->fb.gpu_addr);
  l r=ioctl(o->fd,req,&o->fb.gpu_addr);
  if(ISERR(r))
    PERR("output:%s:dce:unable to free frame buffer memory (LEAK!)\n",sysname);
  else LOG("output:%s:dce:double frame buffer freed successfully\n",sysname);
}

//needed because process death won't cleanup gpu external resources
void dce_outputs_cleanup_force(void)
{
  s32 output_name=0;
  while(1){
    if(output_name==REGISTRY_N_MAX) break;
    if(registry[output_name].itf==WL_OUTPUT){
      if(registry[output_name].output.d==0) continue;
      dce_dpm_on(&registry[output_name].output);
      dce_double_frame_buffer_free(&registry[output_name].output);
    }
    ++output_name;
  }
}
